'torbjoern 26. august 2010
' Boid code taken from a processing.js example:
'http://processingjs.org/learning/topic/flocking
SuperStrict

Framework brl.GLMax2D
Import brl.random
Import brl.standardio

Include "../common/tvector.bmx"

'SetGraphicsDriver GLMax2DDriver()
Graphics 1024,768,0,0,2

Global displayObjects:TList = New TList
Global herbivoreCount:Int = 0
FlushMouse()

Local tim:Int, oldtim:Int
global deltaMillis :int
Local dt#

For Local i:Int = 0 To 10
	Local newHerbivore:THerbivore = New THerbivore 
	newHerbivore.pos.set( GraphicsWidth()/2+Rand(-5,5), GraphicsHeight()/2+Rand(-5,5) )
	displayObjects.addLast( newHerbivore)
Next

Function drawrectopen(x0#,y0#,x1#,y1#)
	DrawLine(x0,y0,x1,y0)
	DrawLine(x1,y0,x1,y1)
	DrawLine(x1,y1,x0,y1)
	DrawLine(x0,y1,x0,y0)
End Function


While Not AppTerminate() And Not KeyHit(KEY_ESCAPE)
	tim = MilliSecs()
	deltaMillis = (tim - oldtim)
	dt = deltaMillis / 1000.0
	oldtim = tim

	herbivoreCount = 0
	For Local b:THerbivore = EachIn displayObjects
		herbivoreCount :+1
		
		b.run( displayObjects )
		b.tryToMultiply(displayobjects)
		
		Local eatenFood:TFood = b.lookForFood(displayobjects)
		If eatenFood <> Null displayObjects.remove(eatenFood)
	Next
	
	
	For Local pred:TPredator = EachIn displayObjects
		
		If pred.isHunting()
			If pred.completedHunt()
				displayObjects.remove(pred.huntingTarget)
				pred.huntingTarget = Null
			EndIf
		Else If pred.isHungry()
			'find closest herbivore to predator
			Local closestHerb:THerbivore = Null
			Local minDist# = 1e6
			For Local herb:THerbivore = EachIn displayObjects
				Local d# = VectorDistance(pred.pos,herb.pos)
				If d < pred.maximumHuntDist
					If d < minDist
						minDist = d
						closestHerb = herb
					EndIf	
				EndIf
			Next
			
			If closestHerb <> Null
				pred.setTarget(closestHerb)
			Else
				pred.walkabout() 'hasnt got any target
			EndIf
		Else 'not hunting or hungry
			pred.vel.set(0,0)
			pred.acc.set(0,0)
			
		EndIf

		pred.updateLife(dt)
		pred.update()
	Next

	draw()
	Delay 5
Wend

Function draw()
	Cls()
	For Local drawable:IDrawable= EachIn displayObjects
		drawable.draw()
	Next
	
	drawrectopen(MouseX(), MouseY(), MouseX()+50, MouseY()+50)
	
	If MouseHit(1)
		Local newPred:TPredator = New TPredator
		newPred.pos.set( MouseX(), MouseY() )
		displayObjects.addLast( newPred )
	EndIf
	
	If MouseHit(2)
		For Local i:Int = 0 To 10
		Local newHerbivore:THerbivore = New THerbivore 
		newHerbivore.pos.set( MouseX()+Rand(-5,5), MouseY() )
		displayObjects.addLast( newHerbivore)
		Next
	EndIf
	
	If MouseHit(3)
		Local newFood:TFood = New TFood 
		newFood.pos.set( MouseX(), MouseY() )
		displayObjects.addLast( newFood)
	EndIf
	
	SetColor 196,196,196
	DrawText "herbivoreCount: " + herbivoreCount, 15,15*1
	DrawText "add predator - mouse 1", 15, 15*2
	DrawText "add 10 herbivores - mouse 2", 15, 15*3
	DrawText "place 1 food - mouse 3", 15, 15*4
	DrawText "herbivores multiply if they have eaten food and collide with another one", 15, 15*5
	DrawText "predators... are a bit daft when they are hunting prey...", 15, 15*6
	DrawText "deltaMillis "+deltaMillis ,15,15*7
	
	Flip()
End Function

Type IDrawable
	Method draw() Abstract 
End Type

Type Critter Extends IDrawable
	Field pos:TVector = New TVector
	Field vel:TVector = New TVector
	Field acc:TVector = New TVector
	
	Field health:Int = 100
End Type

Type TBoid Extends Critter
	Field r#
	Field maxforce#
	Field maxspeed#
	
	Method New()
		r = 3.0
		vel.set( Rnd(-1,1), Rnd(-1,1) )
		maxforce = 0.05 'Maximum steering force  
	End Method
	
	Method run(otherboids:TList)
		flock(otherboids)
		update()
	End Method
	
	'accumulate a new accel
	Method  flock(otherboids:TList)
		Local sep:TVector = separate(otherboids)
		Local ali:TVector = align(otherboids)
		Local coh:TVector = cohesion(otherboids)
	     'Arbitrarily weight these forces  
		sep.mult(2.5)  
		ali.mult(1.0)  
		coh.mult(1.0)  
		'Add the force vectors To acceleration  
		acc.add(sep)
		acc.add(ali) 
		acc.add(coh) 
		acc.add( bounceScreenEdges() ) 
	End Method
	
	'update location
	Method update()
		vel.add(acc)
		vel.limit(maxspeed)
		pos.add(vel)
		acc.set(0,0) 'reset accel each cycle
	End Method
	
	Method seek(target:TVector)
		acc.add( steer(target, False) )
	End Method
	
	Method arrive(target:TVector)
		acc.add( steer(target, True) )
	End Method
	

	'A method that calculates a steering vector towards a target  
	'Takes a second argument, If True, it slows down as it approaches the target  
	Method steer:TVector(target:TVector, slowdown:Int)
		Local steer:TVector = Null
		Local desired:TVector = VectorSub(target,pos)
		Local d# = desired.magnitude()
		If d > 0
			desired.normalize()
			
			'To options for desired vector magnitude.
			If( slowdown And d < 100.0 )
				desired.mult(maxspeed*d/100) '1.based on distance
			Else
				desired.mult(maxspeed) '2.based on maxspeed
			EndIf
			
			steer = vectorsub(desired,vel) 'steering minus velocity
			steer.limit(maxforce)
		Else
			steer = VectorCreate(0,0)
		EndIf
		Return steer
	End Method
	
	'Method checks for nearby boids and steers away  
	Method separate:TVector(otherboids:TList)
		Local desiredSeparation# = 25.0
		Local sum:TVector = VectorCreate(0,0)
		Local count:Int = 0
		'for every boid in the system, check if its too close
		For Local other:TBoid = EachIn otherboids
			'If other=Self Continue
			Local d# = VectorDistance(pos,other.pos)
			If d > 0 And d < desiredSeparation
				'Calculate vector pointing away from neighbor
				Local diff:TVector = VectorSub(pos, other.pos)
				diff.normalize()
				diff.div(d) 'weight by distance
				sum.add(diff)
				count :+1 'keep track of how many are too close
			EndIf
		Next
		
		'average - divide by how many
		If count > 0
			sum.div(Float(count))
		EndIf
		Return sum
	End Method
	
	'For every nearby boid in the system, 
	'calculate the average velocity  
	Method align:TVector(otherboids:TList)
		Local neighborDist# = 50.0
		Local sum:TVector = VectorCreate(0,0)
		Local count:Int = 0
		For Local other:TBoid = EachIn otherboids
			'If other=Self Continue

			Local d# = VectorDistance(pos,other.pos)
			If d > 0 And d < neighborDist
				sum.add(other.vel)
				count :+1
			EndIf
		Next
		
		If count > 0
			sum.div(Float(count))
			sum.limit(maxforce)
		EndIf
		
		Return sum		
	End Method
	
	'For the average location (i.e. center) of all nearby boids,
	'calculate steering vector towards that location  
	Method cohesion:TVector(otherboids:TList)
		Local neighborDist# = 100.0
		Local sum:TVector = VectorCreate(0,0)
		Local count:Int = 0
		For Local other:TBoid = EachIn otherboids
			'If( other=Self ) Continue

			Local d# = VectorDistance(pos,other.pos)
			If d > 0 And d < neighborDist
				sum.add(other.pos)
				count :+1
			EndIf
		Next
		
		If count > 0
			sum.div(Float(count))
			Return steer(sum, False) 'steer towards loc
		EndIf
		
		Return sum
	End Method
	
	
	Method bounceScreenEdges:TVector()
		Local edgeLim# = 50.0
		Local acc:TVector = VectorCreate(0,0)

		If(pos._x < edgeLim) acc._x = pos._x/edgeLim
		If(pos._x > GraphicsWidth()-edgeLim) acc._x = -pos._x/edgeLim
		If(pos._y < edgeLim) acc._y = pos._y/edgeLim
		If(pos._y > GraphicsHeight()-edgeLim) acc._y = -pos._y/edgeLim
				
		Return acc
	End Method
End Type

Type TPredator Extends TBoid
	Field huntCooldown# 'seconds
	Field hasEaten:Int = False
	Field randomWanderTarget:TVector = VectorCreate( Rand(10,GraphicsWidth()), Rand(10,GraphicsHeight()))
	
	Field huntingTarget:THerbivore = Null
	
	Method New()
		maxspeed = 8.0
		huntCooldown = 0.0 'hungry! 
	End Method

	Field siz# = 20
	Field maximumHuntDist# = 80
	
	Method draw()
		If huntCooldown < .1
			SetColor 255,0,0
		Else
			SetColor 128,0,0
		EndIf
		
		If huntCooldown < 0
			siz# = 5
		Else If huntCooldown > 0
			siz# = 5+20*huntCooldown/5.0
		EndIf
		
		
		DrawRect( pos._x-siz/2, pos._y-siz/2, siz,siz )
		drawcircle( pos._x, pos._y, maximumHuntDist, 18)
		If huntCoolDown < 0
			DrawLine pos._x, pos._y, pos._x+vel._x*10, pos._y+vel._y*10
		Else
			SetColor 255,255,255
			DrawText "chomp! chomp! chomp!",pos._x, pos._y-15+Rand(-1,1)
		EndIf
		
		If gotTarget()
			SetColor 255,255,255
			DrawText "!!!",pos._x+Rand(-1,1), pos._y-15+Rand(-1,1)
			DrawLine pos._x, pos._y, huntingTarget.pos._x, huntingTarget.pos._y
		EndIf
		
		If hasEaten 
			hasEaten = False
		EndIf
	End Method
	
	Method updateLife(dt#)
		huntCooldown :- dt
	End Method
	
	Method eat()
		health = 100
		huntCooldown = 5.0
		hasEaten = True
	End Method
	
	Method isHungry:Int()
		Return huntCooldown < 0
	End Method
	
	Method isHunting:Int()
		Return gotTarget()
	End Method
	
	Method gotTarget:Int()
		Return huntingTarget <> Null
	End Method
	
	Method setTarget(herb:THerbivore)
		huntingTarget = herb
	End Method
	
	Method completedHunt:Int()
		Local huntComplete:Int = False
		Local targetNotNull:Int = False
		
		maxspeed = 10
		maxforce = 0.1
		
		Local aheadoftarget:TVector = VectorClone(vel)
		
		aheadoftarget.mult(4)
		aheadoftarget.add( huntingTarget.pos)
		arrive(aheadoftarget)
		

		Local distToTarget# = VectorDistance(pos, huntingTarget.pos)
		Local inRange:Int = distToTarget < (2*r + 2*huntingTarget.r)
		
		If inRange
			eat()
			huntComplete = True
		EndIf
		
		Return huntComplete
	End Method
	
	Method walkabout()
		
		maxspeed = 3
		maxforce = 0.05
	
		If huntCoolDown > 0 Return ' not hungry
		arrive(randomWanderTarget)
		Local targDist# = VectorDistance(pos, randomWanderTarget) 
		If targDist < 2*r
			randomWanderTarget.set( Rand(10,GraphicsWidth()), Rand(10,GraphicsHeight()))
		EndIf
		
	End Method
End Type

Type THerbivore Extends TBoid
	Field hasEaten:Int = False

	Method New()
		maxspeed = 1.0 
	End Method

	Method draw()
		If hasEaten 
			SetColor 255,255,0
		Else
			SetColor 0,255,0
		EndIf
		Local theta# = vel.heading2D() + 90
		SetOrigin pos._x, pos._y
		SetRotation theta
		Local poly:Float[] = [0.0,-2*r,..
					     -r,r*2,..
					     r,r*2]

		DrawPoly(poly)
		'drawcircle( pos._x, pos._y, r/2, 3 )
		SetRotation 0
		SetOrigin 0,0
	End Method
	
	Method lookForFood:TFood(foodlist:TList)
		If hasEaten Return Null
		
		Local eatenFood:TFood = Null
		
		Local closestObj:TFood
		Local minDist# = 1e6
		For Local food:TFood = EachIn foodlist
			Local d# = VectorDistance(Self.pos,food.pos)
			If d < minDist
				minDist = d
				closestObj = food
			EndIf	
		Next
		
		If closestObj <> Null
			If minDist < (r + closestObj.r)
				eatenFood = closestObj
				hasEaten = True
			EndIf
		EndIf
	
		Return eatenFood
	End Method
	
	Method tryToMultiply(herblist:TList)
		If(hasEaten = False) Return
	
		Local closestHerb:THerbivore = Null
		Local minDist# = 1e6
		For Local herb:THerbivore = EachIn herblist
			If herb=Self Continue
			Local d# = VectorDistance(Self.pos,herb.pos)
			If d < minDist
				minDist = d
				closestHerb = herb
			EndIf	
		Next
		
		If closestHerb <> Null And minDist < r*4
			'If( Rnd(0,1) < .5)
			If True
				hasEaten = False
				Local offspring:THerbivore = New THerbivore
				offspring.pos.copy( Self.pos )
				herblist.addLast( offspring)
			EndIf
		EndIf
		
	End Method
End Type

Type ICritterInteractable Extends IDrawable
	Field pos:TVector = New TVector
	Field r#
End Type

Type TFood Extends ICritterInteractable
	Method New()
		r = 30
	End Method

	Method draw()
		SetColor 255,255,0
		DrawOval( pos._x-r/2, pos._y-r/2, r,r )
	End Method	
End Type

Function drawcircle(x#,y#,r#,segs:Int)
	Local oldpx# = x+Cos(0)*r
	Local oldpy# = y+Sin(0)*r
	For Local i:Int = 0 To segs
		Local a# = i*360.0 / Float(segs)
		Local xp# = x+Cos(a)*r
		Local yp# = y+Sin(a)*r
		DrawLine(oldpx,oldpy,xp,yp)
		oldpx = xp
		oldpy = yp
	Next
End Function

